{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "09f7126f",
   "metadata": {},
   "outputs": [],
   "source": [
    "import warnings\n",
    "warnings.filterwarnings('ignore')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "c6ab2b72",
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "import gc\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "import re\n",
    "import time\n",
    "from scipy import stats\n",
    "import matplotlib.pyplot as plt\n",
    "import category_encoders as ce\n",
    "import networkx as nx\n",
    "import pickle\n",
    "import lightgbm as lgb\n",
    "import catboost as cat\n",
    "import xgboost as xgb\n",
    "import seaborn as sns\n",
    "from datetime import timedelta\n",
    "from gensim.models import Word2Vec\n",
    "from io import StringIO\n",
    "from tqdm import tqdm\n",
    "from lightgbm import LGBMClassifier\n",
    "from lightgbm import log_evaluation, early_stopping\n",
    "from sklearn.metrics import roc_curve\n",
    "from scipy.stats import chi2_contingency, pearsonr\n",
    "from sklearn.preprocessing import StandardScaler, OneHotEncoder, LabelEncoder\n",
    "from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer\n",
    "from sklearn.feature_extraction import FeatureHasher\n",
    "from sklearn.model_selection import StratifiedKFold, KFold, train_test_split, GridSearchCV\n",
    "from category_encoders import TargetEncoder\n",
    "from sklearn.decomposition import TruncatedSVD\n",
    "from autogluon.tabular import TabularDataset, TabularPredictor, FeatureMetadata\n",
    "from autogluon.features.generators import AsTypeFeatureGenerator, BulkFeatureGenerator, DropUniqueFeatureGenerator, FillNaFeatureGenerator, PipelineFeatureGenerator\n",
    "from autogluon.features.generators import CategoryFeatureGenerator, IdentityFeatureGenerator, AutoMLPipelineFeatureGenerator\n",
    "from autogluon.common.features.types import R_INT, R_FLOAT\n",
    "from autogluon.core.metrics import make_scorer"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a08d6044",
   "metadata": {},
   "source": [
    "## 数据导入"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "df055add",
   "metadata": {},
   "source": [
    "## 通用导入函数"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "74bcbf7b",
   "metadata": {},
   "outputs": [],
   "source": [
    "def load_data_from_directory(directory):\n",
    "    \"\"\"\n",
    "    遍历目录加载所有CSV文件，将其作为独立的DataFrame变量\n",
    "\n",
    "    参数:\n",
    "    - directory: 输入的数据路径\n",
    "    \n",
    "    返回:\n",
    "    - 含有数据集名称的列表\n",
    "    \"\"\"\n",
    "    dataset_names = []\n",
    "    for filename in os.listdir(directory):\n",
    "        if filename.endswith(\".csv\"):\n",
    "            dataset_name = os.path.splitext(filename)[0] + '_data' # 获取文件名作为变量名\n",
    "            file_path = os.path.join(directory, filename)  # 完整的文件路径\n",
    "            globals()[dataset_name] = pd.read_csv(file_path)  # 将文件加载为DataFrame并赋值给全局变量\n",
    "            dataset_names.append(dataset_name)\n",
    "            print(f\"数据集 {dataset_name} 已加载为 DataFrame\")\n",
    "\n",
    "    return dataset_names"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "993c86ce",
   "metadata": {},
   "source": [
    "## 训练集导入"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "722d1e40",
   "metadata": {},
   "outputs": [],
   "source": [
    "train_load_dt = './data/Train/TRAIN_CCD_TR_DTL.csv'\n",
    "train_data = pd.read_csv(train_load_dt)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "82e70968",
   "metadata": {},
   "source": [
    "## 测试集导入"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "c3ef0f15",
   "metadata": {},
   "outputs": [],
   "source": [
    "A_load_dt = './data/A/A_CCD_TR_DTL.csv'\n",
    "A_data = pd.read_csv(A_load_dt)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b6a9129f",
   "metadata": {},
   "source": [
    "# 贷记卡交易明细表特征工程\n",
    "\n",
    "**数据说明:**\n",
    "- 字段: CUST_NO(客户编号), TR_DAT(交易日期), TR_TIME(交易时间), TR_COD(交易码), TR_AMT(交易金额), TR_DRCT(交易方向), TR_CHANL_CD(交易渠道), MCHT_NO(商户号)\n",
    "- 数据时间范围: 近3个月贷记卡交易流水\n",
    "\n",
    "**特征设计思路:**\n",
    "1. 基础统计特征(交易笔数、金额统计)\n",
    "2. 时间维度特征(月度、周度、小时段、周末特征)\n",
    "3. 交易类型特征(交易码、交易方向、渠道分布)\n",
    "4. 商户维度特征(商户数、商户集中度)\n",
    "5. RFM特征(最近、频次、金额)\n",
    "6. 消费行为特征(大额交易、小额交易、夜间交易)\n",
    "7. 稳定性特征(间隔规律性、金额波动)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e868ba2c",
   "metadata": {},
   "source": [
    "## 1. 数据预处理"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "627d852e",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "开始数据预处理...\n",
      "\n",
      "===== TRAIN数据集预处理 =====\n",
      "原始数据形状: (499346, 8)\n",
      "处理后数据形状: (499346, 22)\n",
      "日期范围: 2013-10-01 ~ 2013-12-31\n",
      "月份分布:\n",
      "months_to_now\n",
      "0    171867\n",
      "1    170050\n",
      "2    157429\n",
      "Name: count, dtype: int64\n",
      "缺失值统计:\n",
      "Series([], dtype: int64)\n",
      "\n",
      "===== A数据集预处理 =====\n",
      "原始数据形状: (499346, 8)\n",
      "处理后数据形状: (499346, 22)\n",
      "日期范围: 2013-10-01 ~ 2013-12-31\n",
      "月份分布:\n",
      "months_to_now\n",
      "0    171867\n",
      "1    170050\n",
      "2    157429\n",
      "Name: count, dtype: int64\n",
      "缺失值统计:\n",
      "Series([], dtype: int64)\n",
      "\n",
      "===== A数据集预处理 =====\n",
      "原始数据形状: (499346, 8)\n",
      "处理后数据形状: (499346, 22)\n",
      "日期范围: 2013-10-01 ~ 2013-12-31\n",
      "月份分布:\n",
      "months_to_now\n",
      "0    171867\n",
      "1    170050\n",
      "2    157429\n",
      "Name: count, dtype: int64\n",
      "缺失值统计:\n",
      "Series([], dtype: int64)\n",
      "处理后数据形状: (499346, 22)\n",
      "日期范围: 2013-10-01 ~ 2013-12-31\n",
      "月份分布:\n",
      "months_to_now\n",
      "0    171867\n",
      "1    170050\n",
      "2    157429\n",
      "Name: count, dtype: int64\n",
      "缺失值统计:\n",
      "Series([], dtype: int64)\n"
     ]
    }
   ],
   "source": [
    "def process_ccd_data(df, dataset_name):\n",
    "    \"\"\"\n",
    "    贷记卡交易数据预处理\n",
    "    \n",
    "    参数:\n",
    "    - df: 原始数据框\n",
    "    - dataset_name: 数据集名称(TRAIN或A)\n",
    "    \n",
    "    返回:\n",
    "    - 处理后的数据框\n",
    "    \"\"\"\n",
    "    df = df.copy()\n",
    "    print(f\"\\n===== {dataset_name}数据集预处理 =====\")\n",
    "    print(f\"原始数据形状: {df.shape}\")\n",
    "    \n",
    "    # 1. 日期时间处理\n",
    "    df['TR_DAT'] = pd.to_datetime(df['TR_DAT'], format='%Y%m%d')\n",
    "    \n",
    "    # 处理TR_TIME字段(float64型)\n",
    "    # Step1: 先转为int整型去除小数位\n",
    "    df['TR_TIME'] = pd.to_numeric(df['TR_TIME'], errors='coerce').fillna(120000).astype(int)\n",
    "    \n",
    "    # Step2: 转为字符串,根据长度判断格式\n",
    "    df['TR_TIME_str'] = df['TR_TIME'].astype(str)\n",
    "    \n",
    "    # Step3: 提取小时和分钟\n",
    "    # 如果是5位数(如75657),第1位是小时,2-3位是分钟\n",
    "    # 如果是6位数(如120000),前2位是小时,3-4位是分钟\n",
    "    def extract_hour_minute(time_str):\n",
    "        time_str = str(time_str).zfill(6)  # 补齐6位\n",
    "        if len(time_str) == 5:\n",
    "            hour = int(time_str[0])\n",
    "            minute = int(time_str[1:3])\n",
    "        elif len(time_str) == 6:\n",
    "            hour = int(time_str[0:2])\n",
    "            minute = int(time_str[2:4])\n",
    "        else:\n",
    "            # 其他情况补齐到6位\n",
    "            time_str = time_str.zfill(6)\n",
    "            hour = int(time_str[0:2])\n",
    "            minute = int(time_str[2:4])\n",
    "        \n",
    "        # 验证时间有效性\n",
    "        if hour > 23:\n",
    "            hour = 12  # 无效小时设为12\n",
    "        if minute > 59:\n",
    "            minute = 0  # 无效分钟设为0\n",
    "        \n",
    "        return hour, minute\n",
    "    \n",
    "    # 应用提取函数\n",
    "    df[['hour', 'minute']] = df['TR_TIME_str'].apply(\n",
    "        lambda x: pd.Series(extract_hour_minute(x))\n",
    "    )\n",
    "    \n",
    "    # 标准化TR_TIME为6位格式\n",
    "    df['TR_TIME'] = df['hour'].astype(str).str.zfill(2) + df['minute'].astype(str).str.zfill(2) + '00'\n",
    "    \n",
    "    # 计算距今天数\n",
    "    max_date = df['TR_DAT'].max()\n",
    "    df['days_to_now'] = (max_date - df['TR_DAT']).dt.days\n",
    "    df['months_to_now'] = df['days_to_now'] // 31\n",
    "    df['weeks_to_now'] = df['days_to_now'] // 7\n",
    "    \n",
    "    # 提取时间特征\n",
    "    df['year'] = df['TR_DAT'].dt.year\n",
    "    df['month'] = df['TR_DAT'].dt.month\n",
    "    df['day'] = df['TR_DAT'].dt.day\n",
    "    df['dayofweek'] = df['TR_DAT'].dt.dayofweek\n",
    "    df['is_weekend'] = (df['dayofweek'] >= 5).astype(int)\n",
    "    \n",
    "    # 时段划分\n",
    "    df['time_period'] = pd.cut(df['hour'], \n",
    "                                bins=[-1, 6, 12, 18, 24], \n",
    "                                labels=['凌晨', '上午', '下午', '晚上'])\n",
    "    df['is_night'] = ((df['hour'] >= 22) | (df['hour'] < 6)).astype(int)\n",
    "    \n",
    "    # 2. 交易金额处理\n",
    "    df['TR_AMT'] = df['TR_AMT'].fillna(0)\n",
    "    df['log_amt'] = np.log1p(df['TR_AMT'])  # 对数变换\n",
    "    \n",
    "    # 3. 类别特征处理\n",
    "    df['TR_COD'] = df['TR_COD'].fillna('UNKNOWN')\n",
    "    df['TR_DRCT'] = df['TR_DRCT'].fillna('UNKNOWN')\n",
    "    df['TR_CHANL_CD'] = df['TR_CHANL_CD'].fillna('UNKNOWN')\n",
    "    df['MCHT_NO'] = df['MCHT_NO'].fillna('UNKNOWN')\n",
    "    \n",
    "    print(f\"处理后数据形状: {df.shape}\")\n",
    "    print(f\"日期范围: {df['TR_DAT'].min().date()} ~ {df['TR_DAT'].max().date()}\")\n",
    "    print(f\"月份分布:\\n{df['months_to_now'].value_counts().sort_index()}\")\n",
    "    print(f\"缺失值统计:\\n{df.isnull().sum()[df.isnull().sum() > 0]}\")\n",
    "    \n",
    "    return df\n",
    "\n",
    "# 处理训练集和测试集\n",
    "print(\"开始数据预处理...\")\n",
    "TRAIN_CCD = process_ccd_data(train_data, 'TRAIN')\n",
    "A_CCD = process_ccd_data(A_data, 'A')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "49bf4281",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>CUST_NO</th>\n",
       "      <th>TR_DAT</th>\n",
       "      <th>TR_TIME</th>\n",
       "      <th>TR_COD</th>\n",
       "      <th>TR_AMT</th>\n",
       "      <th>TR_DRCT</th>\n",
       "      <th>TR_CHANL_CD</th>\n",
       "      <th>MCHT_NO</th>\n",
       "      <th>TR_TIME_str</th>\n",
       "      <th>hour</th>\n",
       "      <th>...</th>\n",
       "      <th>months_to_now</th>\n",
       "      <th>weeks_to_now</th>\n",
       "      <th>year</th>\n",
       "      <th>month</th>\n",
       "      <th>day</th>\n",
       "      <th>dayofweek</th>\n",
       "      <th>is_weekend</th>\n",
       "      <th>time_period</th>\n",
       "      <th>is_night</th>\n",
       "      <th>log_amt</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>584a608715582b0cd6a4739106346b92</td>\n",
       "      <td>2013-11-22</td>\n",
       "      <td>202600</td>\n",
       "      <td>dd47aef98380448a4fa8d2a8f4155276</td>\n",
       "      <td>2.47</td>\n",
       "      <td>1</td>\n",
       "      <td>30b6329d64f560ec60434d0fba757ee0</td>\n",
       "      <td>8a690fa4c2b88ea30a10185c0e6c4932</td>\n",
       "      <td>202618</td>\n",
       "      <td>20</td>\n",
       "      <td>...</td>\n",
       "      <td>1</td>\n",
       "      <td>5</td>\n",
       "      <td>2013</td>\n",
       "      <td>11</td>\n",
       "      <td>22</td>\n",
       "      <td>4</td>\n",
       "      <td>0</td>\n",
       "      <td>晚上</td>\n",
       "      <td>0</td>\n",
       "      <td>1.244155</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>80def5a2a3e5b204317c4351369eee53</td>\n",
       "      <td>2013-12-10</td>\n",
       "      <td>105400</td>\n",
       "      <td>dd47aef98380448a4fa8d2a8f4155276</td>\n",
       "      <td>2.73</td>\n",
       "      <td>1</td>\n",
       "      <td>30b6329d64f560ec60434d0fba757ee0</td>\n",
       "      <td>3bb91ca02260d086b5942f1b530b5562</td>\n",
       "      <td>105455</td>\n",
       "      <td>10</td>\n",
       "      <td>...</td>\n",
       "      <td>0</td>\n",
       "      <td>3</td>\n",
       "      <td>2013</td>\n",
       "      <td>12</td>\n",
       "      <td>10</td>\n",
       "      <td>1</td>\n",
       "      <td>0</td>\n",
       "      <td>上午</td>\n",
       "      <td>0</td>\n",
       "      <td>1.316408</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>1ac79785cedefb88d1eceb0fdf0d322c</td>\n",
       "      <td>2013-10-10</td>\n",
       "      <td>075700</td>\n",
       "      <td>dd47aef98380448a4fa8d2a8f4155276</td>\n",
       "      <td>2.17</td>\n",
       "      <td>1</td>\n",
       "      <td>30b6329d64f560ec60434d0fba757ee0</td>\n",
       "      <td>3bb91ca02260d086b5942f1b530b5562</td>\n",
       "      <td>75745</td>\n",
       "      <td>7</td>\n",
       "      <td>...</td>\n",
       "      <td>2</td>\n",
       "      <td>11</td>\n",
       "      <td>2013</td>\n",
       "      <td>10</td>\n",
       "      <td>10</td>\n",
       "      <td>3</td>\n",
       "      <td>0</td>\n",
       "      <td>上午</td>\n",
       "      <td>0</td>\n",
       "      <td>1.153732</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>e6ea77541b0e361016e0b500f8f15e0d</td>\n",
       "      <td>2013-11-28</td>\n",
       "      <td>105000</td>\n",
       "      <td>dd47aef98380448a4fa8d2a8f4155276</td>\n",
       "      <td>10.80</td>\n",
       "      <td>1</td>\n",
       "      <td>30b6329d64f560ec60434d0fba757ee0</td>\n",
       "      <td>3bb91ca02260d086b5942f1b530b5562</td>\n",
       "      <td>105041</td>\n",
       "      <td>10</td>\n",
       "      <td>...</td>\n",
       "      <td>1</td>\n",
       "      <td>4</td>\n",
       "      <td>2013</td>\n",
       "      <td>11</td>\n",
       "      <td>28</td>\n",
       "      <td>3</td>\n",
       "      <td>0</td>\n",
       "      <td>上午</td>\n",
       "      <td>0</td>\n",
       "      <td>2.468100</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>461ed60fbdc822cdc6d4d5492eec5a52</td>\n",
       "      <td>2013-12-25</td>\n",
       "      <td>075800</td>\n",
       "      <td>e5ab500669b76082b30c86aba86cc829</td>\n",
       "      <td>1.64</td>\n",
       "      <td>1</td>\n",
       "      <td>b26a2071db34cc719e9a4e10889cca03</td>\n",
       "      <td>110bf9d9fef51b2edae8e734d0228600</td>\n",
       "      <td>75825</td>\n",
       "      <td>7</td>\n",
       "      <td>...</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>2013</td>\n",
       "      <td>12</td>\n",
       "      <td>25</td>\n",
       "      <td>2</td>\n",
       "      <td>0</td>\n",
       "      <td>上午</td>\n",
       "      <td>0</td>\n",
       "      <td>0.970779</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>5 rows × 22 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "                            CUST_NO     TR_DAT TR_TIME  \\\n",
       "0  584a608715582b0cd6a4739106346b92 2013-11-22  202600   \n",
       "1  80def5a2a3e5b204317c4351369eee53 2013-12-10  105400   \n",
       "2  1ac79785cedefb88d1eceb0fdf0d322c 2013-10-10  075700   \n",
       "3  e6ea77541b0e361016e0b500f8f15e0d 2013-11-28  105000   \n",
       "4  461ed60fbdc822cdc6d4d5492eec5a52 2013-12-25  075800   \n",
       "\n",
       "                             TR_COD  TR_AMT  TR_DRCT  \\\n",
       "0  dd47aef98380448a4fa8d2a8f4155276    2.47        1   \n",
       "1  dd47aef98380448a4fa8d2a8f4155276    2.73        1   \n",
       "2  dd47aef98380448a4fa8d2a8f4155276    2.17        1   \n",
       "3  dd47aef98380448a4fa8d2a8f4155276   10.80        1   \n",
       "4  e5ab500669b76082b30c86aba86cc829    1.64        1   \n",
       "\n",
       "                        TR_CHANL_CD                           MCHT_NO  \\\n",
       "0  30b6329d64f560ec60434d0fba757ee0  8a690fa4c2b88ea30a10185c0e6c4932   \n",
       "1  30b6329d64f560ec60434d0fba757ee0  3bb91ca02260d086b5942f1b530b5562   \n",
       "2  30b6329d64f560ec60434d0fba757ee0  3bb91ca02260d086b5942f1b530b5562   \n",
       "3  30b6329d64f560ec60434d0fba757ee0  3bb91ca02260d086b5942f1b530b5562   \n",
       "4  b26a2071db34cc719e9a4e10889cca03  110bf9d9fef51b2edae8e734d0228600   \n",
       "\n",
       "  TR_TIME_str  hour  ...  months_to_now  weeks_to_now  year  month  day  \\\n",
       "0      202618    20  ...              1             5  2013     11   22   \n",
       "1      105455    10  ...              0             3  2013     12   10   \n",
       "2       75745     7  ...              2            11  2013     10   10   \n",
       "3      105041    10  ...              1             4  2013     11   28   \n",
       "4       75825     7  ...              0             0  2013     12   25   \n",
       "\n",
       "   dayofweek  is_weekend  time_period  is_night   log_amt  \n",
       "0          4           0           晚上         0  1.244155  \n",
       "1          1           0           上午         0  1.316408  \n",
       "2          3           0           上午         0  1.153732  \n",
       "3          3           0           上午         0  2.468100  \n",
       "4          2           0           上午         0  0.970779  \n",
       "\n",
       "[5 rows x 22 columns]"
      ]
     },
     "execution_count": 20,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "TRAIN_CCD.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8739ff0c",
   "metadata": {},
   "source": [
    "## 2. 基础统计特征"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "c9f4bdd0",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "生成基础统计特征...\n",
      "基础统计特征数: 23\n",
      "\n",
      "生成基础统计特征...\n",
      "基础统计特征数: 23\n",
      "\n",
      "训练集基础特征:\n",
      "                            CUST_NO  ccd_tr_count  ccd_amt_sum  ccd_amt_mean  \\\n",
      "0  0014ca73e629886ae91c6cfa121fb97f             6       137.85     22.975000   \n",
      "1  00280558326faf4e77933b7052a5ccea             2        22.45     11.225000   \n",
      "2  0048bcec86e623fb3988ae65be112360           288       709.25      2.462674   \n",
      "3  00518a9c875abd2d63c5106ee4af1113            18       237.42     13.190000   \n",
      "4  005b44c882927ff3007ee5ba13d77a53           122       517.64      4.242951   \n",
      "\n",
      "   ccd_amt_std  ccd_amt_median  ccd_amt_max  ccd_amt_min  ccd_amt_q25  \\\n",
      "0     0.139821          22.970        23.22        22.84      22.8725   \n",
      "1    10.245977          11.225        18.47         3.98       7.6025   \n",
      "2     2.235901           1.560        15.61         0.23       1.0800   \n",
      "3     5.751946          10.935        21.45         5.57      10.4800   \n",
      "4     3.882557           3.295        20.98         0.72       0.8525   \n",
      "\n",
      "   ccd_amt_q75  ...  ccd_active_days  ccd_first_tr_day  ccd_last_tr_day  \\\n",
      "0      23.0000  ...                6                85               17   \n",
      "1      14.8475  ...                2                 6                4   \n",
      "2       3.0725  ...               75                91                0   \n",
      "3      19.7700  ...                6                91               11   \n",
      "4       6.2700  ...               40                90                8   \n",
      "\n",
      "   ccd_amt_range  ccd_amt_iqr  ccd_amt_cv  ccd_amt_per_trans  \\\n",
      "0           0.38       0.1275    0.006086          22.974996   \n",
      "1          14.49       7.2450    0.912782          11.224994   \n",
      "2          15.38       1.9925    0.907916           2.462674   \n",
      "3          15.88       9.2900    0.436084          13.189999   \n",
      "4          20.26       5.4175    0.915060           4.242951   \n",
      "\n",
      "   ccd_trans_per_day  ccd_amt_per_day  ccd_tr_span_days  \n",
      "0               1.00        22.974996                68  \n",
      "1               1.00        11.224994                 2  \n",
      "2               3.84         9.456667                91  \n",
      "3               3.00        39.569993                80  \n",
      "4               3.05        12.941000                82  \n",
      "\n",
      "[5 rows x 24 columns]\n"
     ]
    }
   ],
   "source": [
    "def gen_basic_features(df):\n",
    "    \"\"\"\n",
    "    生成基础统计特征\n",
    "    \"\"\"\n",
    "    print(\"\\n生成基础统计特征...\")\n",
    "    \n",
    "    features = df.groupby('CUST_NO').agg(\n",
    "        # 交易笔数\n",
    "        ccd_tr_count=('TR_AMT', 'count'),\n",
    "        \n",
    "        # 金额统计\n",
    "        ccd_amt_sum=('TR_AMT', 'sum'),\n",
    "        ccd_amt_mean=('TR_AMT', 'mean'),\n",
    "        ccd_amt_std=('TR_AMT', 'std'),\n",
    "        ccd_amt_median=('TR_AMT', 'median'),\n",
    "        ccd_amt_max=('TR_AMT', 'max'),\n",
    "        ccd_amt_min=('TR_AMT', 'min'),\n",
    "        ccd_amt_q25=('TR_AMT', lambda x: x.quantile(0.25)),\n",
    "        ccd_amt_q75=('TR_AMT', lambda x: x.quantile(0.75)),\n",
    "        ccd_amt_skew=('TR_AMT', lambda x: x.skew()),\n",
    "        ccd_amt_kurt=('TR_AMT', lambda x: x.kurt()),\n",
    "        \n",
    "        # 对数金额统计\n",
    "        ccd_log_amt_mean=('log_amt', 'mean'),\n",
    "        ccd_log_amt_std=('log_amt', 'std'),\n",
    "        \n",
    "        # 活跃天数\n",
    "        ccd_active_days=('TR_DAT', 'nunique'),\n",
    "        \n",
    "        # 时间跨度\n",
    "        ccd_first_tr_day=('days_to_now', 'max'),\n",
    "        ccd_last_tr_day=('days_to_now', 'min'),\n",
    "        \n",
    "    ).reset_index()\n",
    "    \n",
    "    # 派生特征\n",
    "    features['ccd_amt_range'] = features['ccd_amt_max'] - features['ccd_amt_min']\n",
    "    features['ccd_amt_iqr'] = features['ccd_amt_q75'] - features['ccd_amt_q25']\n",
    "    features['ccd_amt_cv'] = features['ccd_amt_std'] / (features['ccd_amt_mean'] + 1e-6)\n",
    "    features['ccd_amt_per_trans'] = features['ccd_amt_sum'] / (features['ccd_tr_count'] + 1e-6)\n",
    "    features['ccd_trans_per_day'] = features['ccd_tr_count'] / (features['ccd_active_days'] + 1e-6)\n",
    "    features['ccd_amt_per_day'] = features['ccd_amt_sum'] / (features['ccd_active_days'] + 1e-6)\n",
    "    features['ccd_tr_span_days'] = features['ccd_first_tr_day'] - features['ccd_last_tr_day']\n",
    "    \n",
    "    print(f\"基础统计特征数: {len(features.columns) - 1}\")\n",
    "    return features\n",
    "\n",
    "# 生成特征\n",
    "TRAIN_basic_fea = gen_basic_features(TRAIN_CCD)\n",
    "A_basic_fea = gen_basic_features(A_CCD)\n",
    "\n",
    "print(\"\\n训练集基础特征:\")\n",
    "print(TRAIN_basic_fea.head())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d1c71df4",
   "metadata": {},
   "source": [
    "## 3. 时间维度特征"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "5b75a839",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "生成时间维度特征...\n",
      "时间维度特征数: 44\n",
      "\n",
      "生成时间维度特征...\n",
      "时间维度特征数: 44\n",
      "\n",
      "训练集时间特征:\n",
      "                            CUST_NO  ccd_month_TR_AMT_count_mean  \\\n",
      "0  0014ca73e629886ae91c6cfa121fb97f                     2.000000   \n",
      "1  00280558326faf4e77933b7052a5ccea                     2.000000   \n",
      "2  0048bcec86e623fb3988ae65be112360                    96.000000   \n",
      "3  00518a9c875abd2d63c5106ee4af1113                     6.000000   \n",
      "4  005b44c882927ff3007ee5ba13d77a53                    40.666667   \n",
      "\n",
      "   ccd_month_TR_AMT_count_std  ccd_month_TR_AMT_count_max  \\\n",
      "0                    0.000000                           2   \n",
      "1                         NaN                           2   \n",
      "2                   27.622455                         125   \n",
      "3                    0.000000                           6   \n",
      "4                   10.969655                          53   \n",
      "\n",
      "   ccd_month_TR_AMT_count_min  ccd_month_TR_AMT_sum_mean  \\\n",
      "0                           2                  45.950000   \n",
      "1                           2                  22.450000   \n",
      "2                          70                 236.416667   \n",
      "3                           6                  79.140000   \n",
      "4                          32                 172.546667   \n",
      "\n",
      "   ccd_month_TR_AMT_sum_std  ccd_month_TR_AMT_sum_max  \\\n",
      "0                  0.208806                     46.19   \n",
      "1                       NaN                     22.45   \n",
      "2                 85.252349                    326.14   \n",
      "3                  0.000000                     79.14   \n",
      "4                 48.544381                    204.02   \n",
      "\n",
      "   ccd_month_TR_AMT_sum_min  ccd_month_TR_AMT_mean_mean  ...  \\\n",
      "0                     45.81                   22.975000  ...   \n",
      "1                     22.45                   11.225000  ...   \n",
      "2                    156.48                    2.427143  ...   \n",
      "3                     79.14                   13.190000  ...   \n",
      "4                    116.64                    4.385830  ...   \n",
      "\n",
      "   ccd_weekend_amt_ratio  ccd_weekend_avg_amt  ccd_weekday_avg_amt  \\\n",
      "0               0.335074            23.094988            22.914994   \n",
      "1               0.000000             0.000000            11.224994   \n",
      "2               0.241903             2.350274             2.500837   \n",
      "3               0.333333            13.189998            13.189999   \n",
      "4               0.397168             5.710833             3.628488   \n",
      "\n",
      "   ccd_night_count  ccd_day_count  ccd_night_amt  ccd_avg_hour  ccd_hour_std  \\\n",
      "0                2              4          45.68     35.500000     36.800815   \n",
      "1                0              2           0.00     12.000000      0.000000   \n",
      "2               80            208         169.77     27.843750     30.666577   \n",
      "3                9              9          82.26      6.333333      5.841031   \n",
      "4               35             87         130.56     26.590164     25.818743   \n",
      "\n",
      "   ccd_night_ratio  ccd_night_amt_ratio  \n",
      "0         0.333333             0.331375  \n",
      "1         0.000000             0.000000  \n",
      "2         0.277778             0.239366  \n",
      "3         0.500000             0.346475  \n",
      "4         0.286885             0.252222  \n",
      "\n",
      "[5 rows x 45 columns]\n"
     ]
    }
   ],
   "source": [
    "def gen_time_features(df):\n",
    "    \"\"\"\n",
    "    生成时间维度特征\n",
    "    \"\"\"\n",
    "    print(\"\\n生成时间维度特征...\")\n",
    "    \n",
    "    # 3.1 月度特征\n",
    "    month_agg = df.groupby(['CUST_NO', 'months_to_now']).agg({\n",
    "        'TR_AMT': ['count', 'sum', 'mean', 'std'],\n",
    "        'TR_DAT': 'nunique'\n",
    "    })\n",
    "    month_agg.columns = ['_'.join(col).strip() for col in month_agg.columns.values]\n",
    "    month_agg = month_agg.reset_index()\n",
    "    \n",
    "    month_features = month_agg.groupby('CUST_NO').agg({\n",
    "        'TR_AMT_count': ['mean', 'std', 'max', 'min'],\n",
    "        'TR_AMT_sum': ['mean', 'std', 'max', 'min'],\n",
    "        'TR_AMT_mean': ['mean', 'std'],\n",
    "        'TR_DAT_nunique': ['mean', 'max']\n",
    "    })\n",
    "    month_features.columns = ['ccd_month_' + '_'.join(col).strip() for col in month_features.columns.values]\n",
    "    month_features = month_features.reset_index()\n",
    "    \n",
    "    # 月度稳定性\n",
    "    month_features['ccd_month_count_cv'] = month_features['ccd_month_TR_AMT_count_std'] / (month_features['ccd_month_TR_AMT_count_mean'] + 1e-6)\n",
    "    month_features['ccd_month_amt_cv'] = month_features['ccd_month_TR_AMT_sum_std'] / (month_features['ccd_month_TR_AMT_sum_mean'] + 1e-6)\n",
    "    \n",
    "    # 最近月份特征(月0、月1、月2)\n",
    "    for month_idx in [0, 1, 2]:\n",
    "        month_data = month_agg[month_agg['months_to_now'] == month_idx]\n",
    "        month_data = month_data[['CUST_NO', 'TR_AMT_count', 'TR_AMT_sum']].rename(\n",
    "            columns={\n",
    "                'TR_AMT_count': f'ccd_month{month_idx}_count',\n",
    "                'TR_AMT_sum': f'ccd_month{month_idx}_amt'\n",
    "            }\n",
    "        )\n",
    "        month_features = month_features.merge(month_data, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 月度增长率\n",
    "    month_features['ccd_month_amt_growth_0_1'] = (month_features['ccd_month0_amt'] - month_features['ccd_month1_amt']) / (month_features['ccd_month1_amt'] + 1e-6)\n",
    "    month_features['ccd_month_amt_growth_1_2'] = (month_features['ccd_month1_amt'] - month_features['ccd_month2_amt']) / (month_features['ccd_month2_amt'] + 1e-6)\n",
    "    month_features['ccd_month_count_growth_0_1'] = (month_features['ccd_month0_count'] - month_features['ccd_month1_count']) / (month_features['ccd_month1_count'] + 1e-6)\n",
    "    \n",
    "    # 3.2 周度特征\n",
    "    week_agg = df.groupby(['CUST_NO', 'weeks_to_now']).agg({\n",
    "        'TR_AMT': ['count', 'sum']\n",
    "    })\n",
    "    week_agg.columns = ['_'.join(col).strip() for col in week_agg.columns.values]\n",
    "    week_agg = week_agg.reset_index()\n",
    "    \n",
    "    week_features = week_agg.groupby('CUST_NO').agg({\n",
    "        'TR_AMT_count': ['mean', 'std', 'max'],\n",
    "        'TR_AMT_sum': ['mean', 'std', 'max']\n",
    "    })\n",
    "    week_features.columns = ['ccd_week_' + '_'.join(col).strip() for col in week_features.columns.values]\n",
    "    week_features = week_features.reset_index()\n",
    "    \n",
    "    # 3.3 工作日/周末特征\n",
    "    weekend_features = df.groupby('CUST_NO').agg(\n",
    "        ccd_weekday_count=('is_weekend', lambda x: (x == 0).sum()),\n",
    "        ccd_weekend_count=('is_weekend', lambda x: (x == 1).sum()),\n",
    "        ccd_weekday_amt=('TR_AMT', lambda x: x[df.loc[x.index, 'is_weekend'] == 0].sum()),\n",
    "        ccd_weekend_amt=('TR_AMT', lambda x: x[df.loc[x.index, 'is_weekend'] == 1].sum()),\n",
    "    ).reset_index()\n",
    "    \n",
    "    weekend_features['ccd_weekend_ratio'] = weekend_features['ccd_weekend_count'] / (weekend_features['ccd_weekday_count'] + weekend_features['ccd_weekend_count'] + 1e-6)\n",
    "    weekend_features['ccd_weekend_amt_ratio'] = weekend_features['ccd_weekend_amt'] / (weekend_features['ccd_weekday_amt'] + weekend_features['ccd_weekend_amt'] + 1e-6)\n",
    "    weekend_features['ccd_weekend_avg_amt'] = weekend_features['ccd_weekend_amt'] / (weekend_features['ccd_weekend_count'] + 1e-6)\n",
    "    weekend_features['ccd_weekday_avg_amt'] = weekend_features['ccd_weekday_amt'] / (weekend_features['ccd_weekday_count'] + 1e-6)\n",
    "    \n",
    "    # 3.4 时段特征\n",
    "    hour_features = df.groupby('CUST_NO').agg(\n",
    "        ccd_night_count=('is_night', lambda x: (x == 1).sum()),\n",
    "        ccd_day_count=('is_night', lambda x: (x == 0).sum()),\n",
    "        ccd_night_amt=('TR_AMT', lambda x: x[df.loc[x.index, 'is_night'] == 1].sum()),\n",
    "        ccd_avg_hour=('hour', 'mean'),\n",
    "        ccd_hour_std=('hour', 'std'),\n",
    "    ).reset_index()\n",
    "    \n",
    "    hour_features['ccd_night_ratio'] = hour_features['ccd_night_count'] / (hour_features['ccd_night_count'] + hour_features['ccd_day_count'] + 1e-6)\n",
    "    hour_features['ccd_night_amt_ratio'] = hour_features['ccd_night_amt'] / (df.groupby('CUST_NO')['TR_AMT'].sum().values + 1e-6)\n",
    "    \n",
    "    # 合并所有时间特征\n",
    "    time_features = month_features.copy()\n",
    "    time_features = time_features.merge(week_features, on='CUST_NO', how='left')\n",
    "    time_features = time_features.merge(weekend_features, on='CUST_NO', how='left')\n",
    "    time_features = time_features.merge(hour_features, on='CUST_NO', how='left')\n",
    "    \n",
    "    print(f\"时间维度特征数: {len(time_features.columns) - 1}\")\n",
    "    return time_features\n",
    "\n",
    "# 生成特征\n",
    "TRAIN_time_fea = gen_time_features(TRAIN_CCD)\n",
    "A_time_fea = gen_time_features(A_CCD)\n",
    "\n",
    "print(\"\\n训练集时间特征:\")\n",
    "print(TRAIN_time_fea.head())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "371c3d47",
   "metadata": {},
   "source": [
    "## 4. 交易类型特征(交易码、方向、渠道)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "cb6e562f",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "生成交易类型特征...\n",
      "交易类型特征数: 20\n",
      "\n",
      "生成交易类型特征...\n",
      "交易类型特征数: 20\n",
      "\n",
      "训练集交易类型特征:\n",
      "                            CUST_NO  ccd_tr_cod_nunique  \\\n",
      "0  0014ca73e629886ae91c6cfa121fb97f                   2   \n",
      "1  00280558326faf4e77933b7052a5ccea                   1   \n",
      "2  0048bcec86e623fb3988ae65be112360                   9   \n",
      "3  00518a9c875abd2d63c5106ee4af1113                   2   \n",
      "4  005b44c882927ff3007ee5ba13d77a53                   7   \n",
      "\n",
      "   ccd_main_tr_cod_ratio  ccd_tr_drct_nunique  ccd_drct_1_TR_AMT_count  \\\n",
      "0               0.500000                    2                      3.0   \n",
      "1               1.000000                    1                      0.0   \n",
      "2               0.427083                    2                    222.0   \n",
      "3               0.500000                    2                      9.0   \n",
      "4               0.565574                    2                     73.0   \n",
      "\n",
      "   ccd_drct_1_TR_AMT_sum  ccd_drct_1_TR_AMT_mean  ccd_drct_-1_TR_AMT_count  \\\n",
      "0                  69.03               23.010000                       3.0   \n",
      "1                   0.00                0.000000                       2.0   \n",
      "2                 598.99                2.698153                      66.0   \n",
      "3                  82.26                9.140000                       9.0   \n",
      "4                 374.17                5.125616                      49.0   \n",
      "\n",
      "   ccd_drct_-1_TR_AMT_sum  ccd_drct_-1_TR_AMT_mean  ...  ccd_main_chanl_ratio  \\\n",
      "0                   68.82                22.940000  ...              0.500000   \n",
      "1                   22.45                11.225000  ...              1.000000   \n",
      "2                  110.26                 1.670606  ...              0.510417   \n",
      "3                  155.16                17.240000  ...              1.000000   \n",
      "4                  143.47                 2.927959  ...              0.622951   \n",
      "\n",
      "   ccd_chanl_30b6329d64f560ec60434d0fba757ee0_count  \\\n",
      "0                                               0.0   \n",
      "1                                               0.0   \n",
      "2                                             147.0   \n",
      "3                                               0.0   \n",
      "4                                              76.0   \n",
      "\n",
      "   ccd_chanl_be33214049497216794466e2704d115f_count  \\\n",
      "0                                               0.0   \n",
      "1                                               0.0   \n",
      "2                                              59.0   \n",
      "3                                               0.0   \n",
      "4                                              37.0   \n",
      "\n",
      "   ccd_chanl_b26a2071db34cc719e9a4e10889cca03_count  \\\n",
      "0                                               3.0   \n",
      "1                                               0.0   \n",
      "2                                              65.0   \n",
      "3                                               0.0   \n",
      "4                                               9.0   \n",
      "\n",
      "   ccd_chanl_5f3bf070208f8beb6f3105098fbe0348_count  \\\n",
      "0                                               3.0   \n",
      "1                                               2.0   \n",
      "2                                               5.0   \n",
      "3                                               0.0   \n",
      "4                                               0.0   \n",
      "\n",
      "   ccd_chanl_84d18a7e959b598b08db2fd3bd7d28f0_count  \\\n",
      "0                                               0.0   \n",
      "1                                               0.0   \n",
      "2                                               0.0   \n",
      "3                                               0.0   \n",
      "4                                               0.0   \n",
      "\n",
      "   ccd_chanl_d19277ce4d534c6a2c1759884e5472bd_count  \\\n",
      "0                                               0.0   \n",
      "1                                               0.0   \n",
      "2                                               0.0   \n",
      "3                                               0.0   \n",
      "4                                               0.0   \n",
      "\n",
      "   ccd_chanl_ce733f44e90fa67e19c9b5dc987983fa_count  \\\n",
      "0                                               0.0   \n",
      "1                                               0.0   \n",
      "2                                               0.0   \n",
      "3                                               0.0   \n",
      "4                                               0.0   \n",
      "\n",
      "   ccd_chanl_9764fe7a437e34df0a39963593925544_count  \\\n",
      "0                                               0.0   \n",
      "1                                               0.0   \n",
      "2                                               0.0   \n",
      "3                                               0.0   \n",
      "4                                               0.0   \n",
      "\n",
      "   ccd_chanl_c919baba7ac93bd2353e12e961ceb31b_count  \n",
      "0                                               0.0  \n",
      "1                                               0.0  \n",
      "2                                               0.0  \n",
      "3                                               0.0  \n",
      "4                                               0.0  \n",
      "\n",
      "[5 rows x 21 columns]\n"
     ]
    }
   ],
   "source": [
    "def gen_transaction_type_features(df):\n",
    "    \"\"\"\n",
    "    生成交易类型特征\n",
    "    \"\"\"\n",
    "    print(\"\\n生成交易类型特征...\")\n",
    "    \n",
    "    # 4.1 交易码特征\n",
    "    tr_cod_features = df.groupby('CUST_NO').agg(\n",
    "        ccd_tr_cod_nunique=('TR_COD', 'nunique'),\n",
    "        ccd_tr_cod_mode=('TR_COD', lambda x: x.mode()[0] if len(x.mode()) > 0 else 'UNKNOWN'),\n",
    "    ).reset_index()\n",
    "    \n",
    "    # 主交易码占比\n",
    "    tr_cod_concentration = df.groupby('CUST_NO')['TR_COD'].apply(\n",
    "        lambda x: x.value_counts().iloc[0] / len(x) if len(x) > 0 else 0\n",
    "    ).reset_index()\n",
    "    tr_cod_concentration.columns = ['CUST_NO', 'ccd_main_tr_cod_ratio']\n",
    "    tr_cod_features = tr_cod_features.merge(tr_cod_concentration, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 4.2 交易方向特征\n",
    "    tr_drct_features = df.groupby('CUST_NO').agg(\n",
    "        ccd_tr_drct_nunique=('TR_DRCT', 'nunique'),\n",
    "    ).reset_index()\n",
    "    \n",
    "    # 各方向统计\n",
    "    for drct in df['TR_DRCT'].unique():\n",
    "        if drct != 'UNKNOWN':\n",
    "            drct_data = df[df['TR_DRCT'] == drct].groupby('CUST_NO').agg({\n",
    "                'TR_AMT': ['count', 'sum', 'mean']\n",
    "            })\n",
    "            drct_data.columns = [f'ccd_drct_{drct}_' + '_'.join(col).strip() for col in drct_data.columns.values]\n",
    "            drct_data = drct_data.reset_index()\n",
    "            tr_drct_features = tr_drct_features.merge(drct_data, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 填充缺失值\n",
    "    tr_drct_features = tr_drct_features.fillna(0)\n",
    "    \n",
    "    # 计算收支比\n",
    "    if 'ccd_drct_D_sum' in tr_drct_features.columns and 'ccd_drct_C_sum' in tr_drct_features.columns:\n",
    "        tr_drct_features['ccd_drct_ratio'] = tr_drct_features['ccd_drct_D_sum'] / (tr_drct_features['ccd_drct_C_sum'] + 1e-6)\n",
    "    \n",
    "    # 4.3 交易渠道特征\n",
    "    tr_chanl_features = df.groupby('CUST_NO').agg(\n",
    "        ccd_tr_chanl_nunique=('TR_CHANL_CD', 'nunique'),\n",
    "    ).reset_index()\n",
    "    \n",
    "    # 主渠道占比\n",
    "    tr_chanl_concentration = df.groupby('CUST_NO')['TR_CHANL_CD'].apply(\n",
    "        lambda x: x.value_counts().iloc[0] / len(x) if len(x) > 0 else 0\n",
    "    ).reset_index()\n",
    "    tr_chanl_concentration.columns = ['CUST_NO', 'ccd_main_chanl_ratio']\n",
    "    tr_chanl_features = tr_chanl_features.merge(tr_chanl_concentration, on='CUST_NO', how='left')\n",
    "    \n",
    "    # Top渠道统计(取前10)\n",
    "    top_chanls = df['TR_CHANL_CD'].value_counts().head(10).index.tolist()\n",
    "    for chanl in top_chanls:\n",
    "        if chanl != 'UNKNOWN':\n",
    "            chanl_count = df[df['TR_CHANL_CD'] == chanl].groupby('CUST_NO').size().reset_index(name=f'ccd_chanl_{chanl}_count')\n",
    "            tr_chanl_features = tr_chanl_features.merge(chanl_count, on='CUST_NO', how='left')\n",
    "    \n",
    "    tr_chanl_features = tr_chanl_features.fillna(0)\n",
    "    \n",
    "    # 合并所有交易类型特征\n",
    "    type_features = tr_cod_features.copy()\n",
    "    type_features = type_features.merge(tr_drct_features, on='CUST_NO', how='left')\n",
    "    type_features = type_features.merge(tr_chanl_features, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 删除非数值列\n",
    "    non_numeric_cols = type_features.select_dtypes(include=['object']).columns\n",
    "    if len(non_numeric_cols) > 1:  # 保留CUST_NO\n",
    "        type_features = type_features.drop(columns=[col for col in non_numeric_cols if col != 'CUST_NO'])\n",
    "    \n",
    "    print(f\"交易类型特征数: {len(type_features.columns) - 1}\")\n",
    "    return type_features\n",
    "\n",
    "# 生成特征\n",
    "TRAIN_type_fea = gen_transaction_type_features(TRAIN_CCD)\n",
    "A_type_fea = gen_transaction_type_features(A_CCD)\n",
    "\n",
    "print(\"\\n训练集交易类型特征:\")\n",
    "print(TRAIN_type_fea.head())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a2826b63",
   "metadata": {},
   "source": [
    "## 5. 商户维度特征"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "3b8cedf5",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "生成商户维度特征...\n",
      "商户维度特征数: 13\n",
      "\n",
      "生成商户维度特征...\n",
      "商户维度特征数: 13\n",
      "\n",
      "训练集商户特征:\n",
      "                            CUST_NO  ccd_mcht_nunique  ccd_mcht_count  \\\n",
      "0  584a608715582b0cd6a4739106346b92               4.0           145.0   \n",
      "1  80def5a2a3e5b204317c4351369eee53              10.0           505.0   \n",
      "2  1ac79785cedefb88d1eceb0fdf0d322c               4.0           335.0   \n",
      "3  e6ea77541b0e361016e0b500f8f15e0d               6.0            59.0   \n",
      "4  461ed60fbdc822cdc6d4d5492eec5a52              10.0           184.0   \n",
      "\n",
      "   ccd_main_mcht_ratio  ccd_max_mcht_amt_ratio  ccd_top3_mcht_amt_ratio  \\\n",
      "0             0.951724                0.918030                 0.986916   \n",
      "1             0.534653                0.516584                 0.783221   \n",
      "2             0.970149                0.942366                 0.995297   \n",
      "3             0.711864                0.653453                 0.851622   \n",
      "4             0.521739                0.444601                 0.864696   \n",
      "\n",
      "   ccd_mcht_freq_mean  ccd_mcht_freq_std  ccd_mcht_freq_max  \\\n",
      "0           36.250000          67.839885              138.0   \n",
      "1           50.500000          82.427409              270.0   \n",
      "2           83.750000         160.842314              325.0   \n",
      "3            9.833333          16.339115               42.0   \n",
      "4           18.400000          34.199415               96.0   \n",
      "\n",
      "   ccd_mcht_freq_min  ccd_mcht_avg_amt_mean  ccd_mcht_avg_amt_std  \\\n",
      "0                1.0               7.209819              4.330134   \n",
      "1                1.0               5.027296              2.641754   \n",
      "2                1.0               5.744163              2.880620   \n",
      "3                1.0              11.442321              6.287485   \n",
      "4                1.0               5.105027              2.520149   \n",
      "\n",
      "   ccd_mcht_avg_amt_max  ccd_avg_amt_per_mcht  \n",
      "0               12.4200             36.249991  \n",
      "1               11.1600             50.499995  \n",
      "2                9.8725             83.749979  \n",
      "3               19.6200              9.833332  \n",
      "4                9.0650             18.399998  \n"
     ]
    }
   ],
   "source": [
    "def gen_merchant_features(df):\n",
    "    \"\"\"\n",
    "    生成商户维度特征\n",
    "    \"\"\"\n",
    "    print(\"\\n生成商户维度特征...\")\n",
    "    \n",
    "    # 过滤掉UNKNOWN商户\n",
    "    df_valid_mcht = df[df['MCHT_NO'] != 'UNKNOWN'].copy()\n",
    "    \n",
    "    # 5.1 基础商户特征\n",
    "    mcht_features = df_valid_mcht.groupby('CUST_NO').agg(\n",
    "        ccd_mcht_nunique=('MCHT_NO', 'nunique'),\n",
    "        ccd_mcht_count=('MCHT_NO', 'count'),\n",
    "    ).reset_index()\n",
    "    \n",
    "    # 商户集中度(主商户占比)\n",
    "    mcht_concentration = df_valid_mcht.groupby('CUST_NO')['MCHT_NO'].apply(\n",
    "        lambda x: x.value_counts().iloc[0] / len(x) if len(x) > 0 else 0\n",
    "    ).reset_index()\n",
    "    mcht_concentration.columns = ['CUST_NO', 'ccd_main_mcht_ratio']\n",
    "    mcht_features = mcht_features.merge(mcht_concentration, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 5.2 商户金额统计\n",
    "    mcht_amt = df_valid_mcht.groupby(['CUST_NO', 'MCHT_NO'])['TR_AMT'].agg(['sum', 'mean', 'count']).reset_index()\n",
    "    \n",
    "    # 最大商户金额占比\n",
    "    max_mcht_amt = mcht_amt.groupby('CUST_NO').apply(\n",
    "        lambda x: x.loc[x['sum'].idxmax(), 'sum'] / x['sum'].sum() if len(x) > 0 else 0\n",
    "    ).reset_index()\n",
    "    max_mcht_amt.columns = ['CUST_NO', 'ccd_max_mcht_amt_ratio']\n",
    "    mcht_features = mcht_features.merge(max_mcht_amt, on='CUST_NO', how='left')\n",
    "    \n",
    "    # Top3商户金额占比\n",
    "    top3_mcht_amt = mcht_amt.groupby('CUST_NO').apply(\n",
    "        lambda x: x.nlargest(min(3, len(x)), 'sum')['sum'].sum() / x['sum'].sum() if len(x) > 0 else 0\n",
    "    ).reset_index()\n",
    "    top3_mcht_amt.columns = ['CUST_NO', 'ccd_top3_mcht_amt_ratio']\n",
    "    mcht_features = mcht_features.merge(top3_mcht_amt, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 5.3 商户交易频率\n",
    "    mcht_freq = mcht_amt.groupby('CUST_NO')['count'].agg(['mean', 'std', 'max', 'min']).reset_index()\n",
    "    mcht_freq.columns = ['CUST_NO', 'ccd_mcht_freq_mean', 'ccd_mcht_freq_std', 'ccd_mcht_freq_max', 'ccd_mcht_freq_min']\n",
    "    mcht_features = mcht_features.merge(mcht_freq, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 商户平均消费金额\n",
    "    mcht_avg_amt = mcht_amt.groupby('CUST_NO')['mean'].agg(['mean', 'std', 'max']).reset_index()\n",
    "    mcht_avg_amt.columns = ['CUST_NO', 'ccd_mcht_avg_amt_mean', 'ccd_mcht_avg_amt_std', 'ccd_mcht_avg_amt_max']\n",
    "    mcht_features = mcht_features.merge(mcht_avg_amt, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 5.4 派生特征\n",
    "    mcht_features['ccd_avg_amt_per_mcht'] = mcht_features['ccd_mcht_count'].astype(float) / (mcht_features['ccd_mcht_nunique'] + 1e-6)\n",
    "    \n",
    "    # 对于没有有效商户的客户,填充为0\n",
    "    all_cust = df[['CUST_NO']].drop_duplicates()\n",
    "    mcht_features = all_cust.merge(mcht_features, on='CUST_NO', how='left').fillna(0)\n",
    "    \n",
    "    print(f\"商户维度特征数: {len(mcht_features.columns) - 1}\")\n",
    "    return mcht_features\n",
    "\n",
    "# 生成特征\n",
    "TRAIN_mcht_fea = gen_merchant_features(TRAIN_CCD)\n",
    "A_mcht_fea = gen_merchant_features(A_CCD)\n",
    "\n",
    "print(\"\\n训练集商户特征:\")\n",
    "print(TRAIN_mcht_fea.head())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5d16d2e5",
   "metadata": {},
   "source": [
    "## 6. 消费行为特征"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "ccc7a46e",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "生成消费行为特征...\n",
      "消费行为特征数: 18\n",
      "\n",
      "生成消费行为特征...\n",
      "消费行为特征数: 18\n",
      "\n",
      "训练集消费行为特征:\n",
      "                            CUST_NO  ccd_large_trans_count  \\\n",
      "0  0014ca73e629886ae91c6cfa121fb97f                    0.0   \n",
      "1  00280558326faf4e77933b7052a5ccea                    0.0   \n",
      "2  0048bcec86e623fb3988ae65be112360                   12.0   \n",
      "3  00518a9c875abd2d63c5106ee4af1113                    0.0   \n",
      "4  005b44c882927ff3007ee5ba13d77a53                    8.0   \n",
      "\n",
      "   ccd_large_trans_ratio  ccd_small_trans_count  ccd_small_trans_ratio  \\\n",
      "0               0.000000                    0.0               0.000000   \n",
      "1               0.000000                    1.0               0.500000   \n",
      "2               0.041667                    7.0               0.024306   \n",
      "3               0.000000                    3.0               0.166667   \n",
      "4               0.065574                    0.0               0.000000   \n",
      "\n",
      "   ccd_top3_amt_sum  ccd_top3_amt_ratio  ccd_top5_amt_ratio  \\\n",
      "0             69.20            0.501995            0.834313   \n",
      "1             22.45            1.000000            1.000000   \n",
      "2             43.13            0.060811            0.097511   \n",
      "3             64.35            0.271039            0.437579   \n",
      "4             51.94            0.100340            0.158701   \n",
      "\n",
      "   ccd_top10_amt_ratio  ccd_bottom50_amt_ratio  ccd_interval_mean  \\\n",
      "0             1.000000                0.498005          13.600000   \n",
      "1             1.000000                0.177283           2.000000   \n",
      "2             0.161170                0.233275           0.317073   \n",
      "3             0.708744                0.335481           4.705882   \n",
      "4             0.274959                0.183293           0.677686   \n",
      "\n",
      "   ccd_interval_std  ccd_interval_max  ccd_interval_min  ccd_interval_median  \\\n",
      "0         14.240786              29.0               1.0                  7.0   \n",
      "1               NaN               2.0               2.0                  2.0   \n",
      "2          0.609213               4.0               0.0                  0.0   \n",
      "3          7.792342              19.0               0.0                  0.0   \n",
      "4          1.361463               9.0               0.0                  0.0   \n",
      "\n",
      "   ccd_interval_cv  ccd_day_regularity_std  ccd_day_regularity_max  \\\n",
      "0         1.047117                0.447214                       2   \n",
      "1              NaN                0.000000                       1   \n",
      "2         1.921360                5.698789                      50   \n",
      "3         1.655872                1.732051                       6   \n",
      "4         2.008985                4.429339                      25   \n",
      "\n",
      "   ccd_day_regularity_max_ratio  \n",
      "0                      0.333333  \n",
      "1                      0.500000  \n",
      "2                      0.173611  \n",
      "3                      0.333333  \n",
      "4                      0.204918  \n"
     ]
    }
   ],
   "source": [
    "def gen_behavior_features(df):\n",
    "    \"\"\"\n",
    "    生成消费行为特征\n",
    "    \"\"\"\n",
    "    print(\"\\n生成消费行为特征...\")\n",
    "    \n",
    "    def compute_behavior(group):\n",
    "        amt = group['TR_AMT'].values\n",
    "        n = len(amt)\n",
    "        \n",
    "        if n == 0:\n",
    "            return pd.Series({\n",
    "                'ccd_large_trans_count': 0,\n",
    "                'ccd_large_trans_ratio': 0,\n",
    "                'ccd_small_trans_count': 0,\n",
    "                'ccd_small_trans_ratio': 0,\n",
    "                'ccd_top3_amt_sum': 0,\n",
    "                'ccd_top3_amt_ratio': 0,\n",
    "                'ccd_top5_amt_ratio': 0,\n",
    "                'ccd_top10_amt_ratio': 0,\n",
    "                'ccd_bottom50_amt_ratio': 0,\n",
    "            })\n",
    "        \n",
    "        mean_amt = amt.mean()\n",
    "        std_amt = amt.std() if n > 1 else 0\n",
    "        total_amt = amt.sum()\n",
    "        \n",
    "        # 大额交易(>均值+2std)\n",
    "        large_threshold = mean_amt + 2 * std_amt\n",
    "        large_count = (amt > large_threshold).sum()\n",
    "        large_ratio = large_count / n\n",
    "        \n",
    "        # 小额交易(<均值-std或<均值的50%)\n",
    "        small_threshold = min(mean_amt * 0.5, mean_amt - std_amt) if std_amt > 0 else mean_amt * 0.5\n",
    "        small_count = (amt < small_threshold).sum()\n",
    "        small_ratio = small_count / n\n",
    "        \n",
    "        # Top交易集中度\n",
    "        sorted_amt = np.sort(amt)[::-1]\n",
    "        top3_sum = sorted_amt[:min(3, n)].sum()\n",
    "        top3_ratio = top3_sum / total_amt if total_amt > 0 else 0\n",
    "        \n",
    "        top5_sum = sorted_amt[:min(5, n)].sum()\n",
    "        top5_ratio = top5_sum / total_amt if total_amt > 0 else 0\n",
    "        \n",
    "        top10_sum = sorted_amt[:min(10, n)].sum()\n",
    "        top10_ratio = top10_sum / total_amt if total_amt > 0 else 0\n",
    "        \n",
    "        # Bottom50%交易占比\n",
    "        bottom_idx = int(n * 0.5)\n",
    "        bottom50_sum = sorted_amt[bottom_idx:].sum()\n",
    "        bottom50_ratio = bottom50_sum / total_amt if total_amt > 0 else 0\n",
    "        \n",
    "        return pd.Series({\n",
    "            'ccd_large_trans_count': large_count,\n",
    "            'ccd_large_trans_ratio': large_ratio,\n",
    "            'ccd_small_trans_count': small_count,\n",
    "            'ccd_small_trans_ratio': small_ratio,\n",
    "            'ccd_top3_amt_sum': top3_sum,\n",
    "            'ccd_top3_amt_ratio': top3_ratio,\n",
    "            'ccd_top5_amt_ratio': top5_ratio,\n",
    "            'ccd_top10_amt_ratio': top10_ratio,\n",
    "            'ccd_bottom50_amt_ratio': bottom50_ratio,\n",
    "        })\n",
    "    \n",
    "    behavior_features = df.groupby('CUST_NO').apply(compute_behavior).reset_index()\n",
    "    \n",
    "    # 6.2 交易间隔特征\n",
    "    df_sorted = df.sort_values(['CUST_NO', 'TR_DAT', 'TR_TIME']).copy()\n",
    "    df_sorted['tr_interval'] = df_sorted.groupby('CUST_NO')['TR_DAT'].diff().dt.days\n",
    "    \n",
    "    interval_features = df_sorted.groupby('CUST_NO')['tr_interval'].agg([\n",
    "        ('ccd_interval_mean', 'mean'),\n",
    "        ('ccd_interval_std', 'std'),\n",
    "        ('ccd_interval_max', 'max'),\n",
    "        ('ccd_interval_min', 'min'),\n",
    "        ('ccd_interval_median', 'median'),\n",
    "    ]).reset_index()\n",
    "    \n",
    "    interval_features['ccd_interval_cv'] = interval_features['ccd_interval_std'] / (interval_features['ccd_interval_mean'] + 1e-6)\n",
    "    \n",
    "    behavior_features = behavior_features.merge(interval_features, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 6.3 消费规律性(每周固定日期消费)\n",
    "    day_dist = df.groupby(['CUST_NO', 'dayofweek']).size().reset_index(name='day_count')\n",
    "    day_regularity = day_dist.groupby('CUST_NO')['day_count'].agg([\n",
    "        ('ccd_day_regularity_std', 'std'),\n",
    "        ('ccd_day_regularity_max', 'max'),\n",
    "    ]).reset_index()\n",
    "    day_regularity['ccd_day_regularity_max_ratio'] = day_regularity['ccd_day_regularity_max'] / df.groupby('CUST_NO').size().values\n",
    "    \n",
    "    behavior_features = behavior_features.merge(day_regularity, on='CUST_NO', how='left')\n",
    "    \n",
    "    print(f\"消费行为特征数: {len(behavior_features.columns) - 1}\")\n",
    "    return behavior_features\n",
    "\n",
    "# 生成特征\n",
    "TRAIN_behavior_fea = gen_behavior_features(TRAIN_CCD)\n",
    "A_behavior_fea = gen_behavior_features(A_CCD)\n",
    "\n",
    "print(\"\\n训练集消费行为特征:\")\n",
    "print(TRAIN_behavior_fea.head())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e683b61d",
   "metadata": {},
   "source": [
    "## 7. RFM特征与趋势特征"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "ebf5d350",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "生成RFM与趋势特征...\n",
      "RFM与趋势特征数: 26\n",
      "\n",
      "生成RFM与趋势特征...\n",
      "RFM与趋势特征数: 26\n",
      "\n",
      "训练集RFM特征:\n",
      "                            CUST_NO  ccd_recency  ccd_frequency  ccd_monetary  \\\n",
      "0  0014ca73e629886ae91c6cfa121fb97f           17              6        137.85   \n",
      "1  00280558326faf4e77933b7052a5ccea            4              2         22.45   \n",
      "2  0048bcec86e623fb3988ae65be112360            0            288        709.25   \n",
      "3  00518a9c875abd2d63c5106ee4af1113           11             18        237.42   \n",
      "4  005b44c882927ff3007ee5ba13d77a53            8            122        517.64   \n",
      "\n",
      "   ccd_rfm_score  ccd_recent7d_count  ccd_recent7d_sum  ccd_recent7d_mean  \\\n",
      "0       0.250688                 0.0              0.00           0.000000   \n",
      "1       0.288319                 2.0             22.45          11.225000   \n",
      "2       0.319841                19.0             58.89           3.099474   \n",
      "3       0.270240                 0.0              0.00           0.000000   \n",
      "4       0.286654                 0.0              0.00           0.000000   \n",
      "\n",
      "   ccd_recent14d_count  ccd_recent14d_sum  ...  ccd_trend_count_30_60  \\\n",
      "0                  0.0               0.00  ...                    0.0   \n",
      "1                  2.0              22.45  ...                    2.0   \n",
      "2                 48.0             133.28  ...                   23.0   \n",
      "3                  3.0              51.72  ...                   -3.0   \n",
      "4                  3.0              11.10  ...                  -19.0   \n",
      "\n",
      "   ccd_trend_amt_30_60  ccd_trend_ratio_count_30_60  \\\n",
      "0                 0.38                 9.999995e-01   \n",
      "1                22.45                 2.000000e+06   \n",
      "2                75.62                 1.239583e+00   \n",
      "3               -27.42                 4.999999e-01   \n",
      "4                -3.39                 6.274510e-01   \n",
      "\n",
      "   ccd_trend_ratio_amt_30_60  ccd_trend_count_7_30  ccd_trend_amt_7_30  \\\n",
      "0               1.008295e+00                  -2.0              -46.19   \n",
      "1               2.245000e+07                   2.0               22.45   \n",
      "2               1.322336e+00                 -81.0             -192.44   \n",
      "3               6.535254e-01                  -3.0              -51.72   \n",
      "4               9.830813e-01                 -32.0             -196.98   \n",
      "\n",
      "   ccd_recent30d_count_ratio  ccd_recent30d_amt_ratio  \\\n",
      "0                   0.333333                 0.335074   \n",
      "1                   1.000000                 1.000000   \n",
      "2                   0.413194                 0.437392   \n",
      "3                   0.166667                 0.217842   \n",
      "4                   0.262295                 0.380535   \n",
      "\n",
      "   ccd_recent7d_count_ratio  ccd_recent7d_amt_ratio  \n",
      "0                  0.000000                0.000000  \n",
      "1                  1.000000                1.000000  \n",
      "2                  0.065972                0.083031  \n",
      "3                  0.000000                0.000000  \n",
      "4                  0.000000                0.000000  \n",
      "\n",
      "[5 rows x 27 columns]\n"
     ]
    }
   ],
   "source": [
    "def gen_rfm_and_trend_features(df):\n",
    "    \"\"\"\n",
    "    生成RFM特征与趋势特征\n",
    "    \"\"\"\n",
    "    print(\"\\n生成RFM与趋势特征...\")\n",
    "    \n",
    "    # 7.1 RFM基础特征\n",
    "    rfm_features = df.groupby('CUST_NO').agg(\n",
    "        ccd_recency=('days_to_now', 'min'),  # 最近一次交易距今天数(R)\n",
    "        ccd_frequency=('TR_AMT', 'count'),    # 交易频次(F)\n",
    "        ccd_monetary=('TR_AMT', 'sum'),       # 交易总金额(M)\n",
    "    ).reset_index()\n",
    "    \n",
    "    # RFM评分(简化版)\n",
    "    rfm_features['ccd_rfm_score'] = (\n",
    "        (100 - rfm_features['ccd_recency']) / 100 * 0.3 +  # R越小越好\n",
    "        rfm_features['ccd_frequency'] / rfm_features['ccd_frequency'].max() * 0.3 +  # F越大越好\n",
    "        rfm_features['ccd_monetary'] / rfm_features['ccd_monetary'].max() * 0.4  # M越大越好\n",
    "    )\n",
    "    \n",
    "    # 7.2 最近N天特征\n",
    "    for days in [7, 14, 30, 60]:\n",
    "        recent_data = df[df['days_to_now'] < days]\n",
    "        recent_features = recent_data.groupby('CUST_NO').agg({\n",
    "            'TR_AMT': ['count', 'sum', 'mean']\n",
    "        })\n",
    "        recent_features.columns = [f'ccd_recent{days}d_{col}' for col in ['count', 'sum', 'mean']]\n",
    "        recent_features = recent_features.reset_index()\n",
    "        rfm_features = rfm_features.merge(recent_features, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 填充缺失值(最近N天没有交易的填0)\n",
    "    rfm_features = rfm_features.fillna(0)\n",
    "    \n",
    "    # 7.3 趋势特征(近期vs早期)\n",
    "    # 近30天 vs 前60天\n",
    "    if 'ccd_recent30d_count' in rfm_features.columns and 'ccd_recent60d_count' in rfm_features.columns:\n",
    "        rfm_features['ccd_trend_count_30_60'] = (rfm_features['ccd_recent30d_count'] - \n",
    "                                                  (rfm_features['ccd_recent60d_count'] - rfm_features['ccd_recent30d_count']))\n",
    "        rfm_features['ccd_trend_amt_30_60'] = (rfm_features['ccd_recent30d_sum'] - \n",
    "                                                (rfm_features['ccd_recent60d_sum'] - rfm_features['ccd_recent30d_sum']))\n",
    "        \n",
    "        rfm_features['ccd_trend_ratio_count_30_60'] = rfm_features['ccd_recent30d_count'] / (\n",
    "            rfm_features['ccd_recent60d_count'] - rfm_features['ccd_recent30d_count'] + 1e-6)\n",
    "        rfm_features['ccd_trend_ratio_amt_30_60'] = rfm_features['ccd_recent30d_sum'] / (\n",
    "            rfm_features['ccd_recent60d_sum'] - rfm_features['ccd_recent30d_sum'] + 1e-6)\n",
    "    \n",
    "    # 近7天 vs 前30天\n",
    "    if 'ccd_recent7d_count' in rfm_features.columns and 'ccd_recent30d_count' in rfm_features.columns:\n",
    "        rfm_features['ccd_trend_count_7_30'] = (rfm_features['ccd_recent7d_count'] - \n",
    "                                                 (rfm_features['ccd_recent30d_count'] - rfm_features['ccd_recent7d_count']))\n",
    "        rfm_features['ccd_trend_amt_7_30'] = (rfm_features['ccd_recent7d_sum'] - \n",
    "                                               (rfm_features['ccd_recent30d_sum'] - rfm_features['ccd_recent7d_sum']))\n",
    "    \n",
    "    # 7.4 活跃度变化\n",
    "    # 最近30天交易占比\n",
    "    rfm_features['ccd_recent30d_count_ratio'] = rfm_features['ccd_recent30d_count'] / (rfm_features['ccd_frequency'] + 1e-6)\n",
    "    rfm_features['ccd_recent30d_amt_ratio'] = rfm_features['ccd_recent30d_sum'] / (rfm_features['ccd_monetary'] + 1e-6)\n",
    "    \n",
    "    # 最近7天交易占比\n",
    "    rfm_features['ccd_recent7d_count_ratio'] = rfm_features['ccd_recent7d_count'] / (rfm_features['ccd_frequency'] + 1e-6)\n",
    "    rfm_features['ccd_recent7d_amt_ratio'] = rfm_features['ccd_recent7d_sum'] / (rfm_features['ccd_monetary'] + 1e-6)\n",
    "    \n",
    "    print(f\"RFM与趋势特征数: {len(rfm_features.columns) - 1}\")\n",
    "    return rfm_features\n",
    "\n",
    "# 生成特征\n",
    "TRAIN_rfm_fea = gen_rfm_and_trend_features(TRAIN_CCD)\n",
    "A_rfm_fea = gen_rfm_and_trend_features(A_CCD)\n",
    "\n",
    "print(\"\\n训练集RFM特征:\")\n",
    "print(TRAIN_rfm_fea.head())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f1f20919",
   "metadata": {},
   "source": [
    "## 8. 特征合并与保存"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "b4dee772",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "合并所有特征...\n",
      "最终特征数: 144\n",
      "客户数: 7332\n",
      "数据形状: (7332, 145)\n",
      "\n",
      "特征数据类型分布:\n",
      "float64    125\n",
      "int64       19\n",
      "object       1\n",
      "Name: count, dtype: int64\n",
      "\n",
      "合并所有特征...\n",
      "最终特征数: 144\n",
      "客户数: 7332\n",
      "数据形状: (7332, 145)\n",
      "\n",
      "特征数据类型分布:\n",
      "float64    125\n",
      "int64       19\n",
      "object       1\n",
      "Name: count, dtype: int64\n",
      "\n",
      "训练集特征预览:\n",
      "                            CUST_NO  ccd_tr_count  ccd_amt_sum  ccd_amt_mean  \\\n",
      "0  0014ca73e629886ae91c6cfa121fb97f             6       137.85     22.975000   \n",
      "1  00280558326faf4e77933b7052a5ccea             2        22.45     11.225000   \n",
      "2  0048bcec86e623fb3988ae65be112360           288       709.25      2.462674   \n",
      "3  00518a9c875abd2d63c5106ee4af1113            18       237.42     13.190000   \n",
      "4  005b44c882927ff3007ee5ba13d77a53           122       517.64      4.242951   \n",
      "\n",
      "   ccd_amt_std  ccd_amt_median  ccd_amt_max  ccd_amt_min  ccd_amt_q25  \\\n",
      "0     0.139821          22.970        23.22        22.84      22.8725   \n",
      "1    10.245977          11.225        18.47         3.98       7.6025   \n",
      "2     2.235901           1.560        15.61         0.23       1.0800   \n",
      "3     5.751946          10.935        21.45         5.57      10.4800   \n",
      "4     3.882557           3.295        20.98         0.72       0.8525   \n",
      "\n",
      "   ccd_amt_q75  ...  ccd_trend_count_30_60  ccd_trend_amt_30_60  \\\n",
      "0      23.0000  ...                    0.0                 0.38   \n",
      "1      14.8475  ...                    2.0                22.45   \n",
      "2       3.0725  ...                   23.0                75.62   \n",
      "3      19.7700  ...                   -3.0               -27.42   \n",
      "4       6.2700  ...                  -19.0                -3.39   \n",
      "\n",
      "   ccd_trend_ratio_count_30_60  ccd_trend_ratio_amt_30_60  \\\n",
      "0                 9.999995e-01               1.008295e+00   \n",
      "1                 2.000000e+06               2.245000e+07   \n",
      "2                 1.239583e+00               1.322336e+00   \n",
      "3                 4.999999e-01               6.535254e-01   \n",
      "4                 6.274510e-01               9.830813e-01   \n",
      "\n",
      "   ccd_trend_count_7_30  ccd_trend_amt_7_30  ccd_recent30d_count_ratio  \\\n",
      "0                  -2.0              -46.19                   0.333333   \n",
      "1                   2.0               22.45                   1.000000   \n",
      "2                 -81.0             -192.44                   0.413194   \n",
      "3                  -3.0              -51.72                   0.166667   \n",
      "4                 -32.0             -196.98                   0.262295   \n",
      "\n",
      "   ccd_recent30d_amt_ratio  ccd_recent7d_count_ratio  ccd_recent7d_amt_ratio  \n",
      "0                 0.335074                  0.000000                0.000000  \n",
      "1                 1.000000                  1.000000                1.000000  \n",
      "2                 0.437392                  0.065972                0.083031  \n",
      "3                 0.217842                  0.000000                0.000000  \n",
      "4                 0.380535                  0.000000                0.000000  \n",
      "\n",
      "[5 rows x 145 columns]\n",
      "\n",
      "测试集特征预览:\n",
      "                            CUST_NO  ccd_tr_count  ccd_amt_sum  ccd_amt_mean  \\\n",
      "0  0014ca73e629886ae91c6cfa121fb97f             6       137.85     22.975000   \n",
      "1  00280558326faf4e77933b7052a5ccea             2        22.45     11.225000   \n",
      "2  0048bcec86e623fb3988ae65be112360           288       709.25      2.462674   \n",
      "3  00518a9c875abd2d63c5106ee4af1113            18       237.42     13.190000   \n",
      "4  005b44c882927ff3007ee5ba13d77a53           122       517.64      4.242951   \n",
      "\n",
      "   ccd_amt_std  ccd_amt_median  ccd_amt_max  ccd_amt_min  ccd_amt_q25  \\\n",
      "0     0.139821          22.970        23.22        22.84      22.8725   \n",
      "1    10.245977          11.225        18.47         3.98       7.6025   \n",
      "2     2.235901           1.560        15.61         0.23       1.0800   \n",
      "3     5.751946          10.935        21.45         5.57      10.4800   \n",
      "4     3.882557           3.295        20.98         0.72       0.8525   \n",
      "\n",
      "   ccd_amt_q75  ...  ccd_trend_count_30_60  ccd_trend_amt_30_60  \\\n",
      "0      23.0000  ...                    0.0                 0.38   \n",
      "1      14.8475  ...                    2.0                22.45   \n",
      "2       3.0725  ...                   23.0                75.62   \n",
      "3      19.7700  ...                   -3.0               -27.42   \n",
      "4       6.2700  ...                  -19.0                -3.39   \n",
      "\n",
      "   ccd_trend_ratio_count_30_60  ccd_trend_ratio_amt_30_60  \\\n",
      "0                 9.999995e-01               1.008295e+00   \n",
      "1                 2.000000e+06               2.245000e+07   \n",
      "2                 1.239583e+00               1.322336e+00   \n",
      "3                 4.999999e-01               6.535254e-01   \n",
      "4                 6.274510e-01               9.830813e-01   \n",
      "\n",
      "   ccd_trend_count_7_30  ccd_trend_amt_7_30  ccd_recent30d_count_ratio  \\\n",
      "0                  -2.0              -46.19                   0.333333   \n",
      "1                   2.0               22.45                   1.000000   \n",
      "2                 -81.0             -192.44                   0.413194   \n",
      "3                  -3.0              -51.72                   0.166667   \n",
      "4                 -32.0             -196.98                   0.262295   \n",
      "\n",
      "   ccd_recent30d_amt_ratio  ccd_recent7d_count_ratio  ccd_recent7d_amt_ratio  \n",
      "0                 0.335074                  0.000000                0.000000  \n",
      "1                 1.000000                  1.000000                1.000000  \n",
      "2                 0.437392                  0.065972                0.083031  \n",
      "3                 0.217842                  0.000000                0.000000  \n",
      "4                 0.380535                  0.000000                0.000000  \n",
      "\n",
      "[5 rows x 145 columns]\n"
     ]
    }
   ],
   "source": [
    "def merge_all_features(basic_fea, time_fea, type_fea, mcht_fea, behavior_fea, rfm_fea):\n",
    "    \"\"\"\n",
    "    合并所有特征\n",
    "    \"\"\"\n",
    "    print(\"\\n合并所有特征...\")\n",
    "    \n",
    "    # 逐步合并\n",
    "    all_features = basic_fea.copy()\n",
    "    all_features = all_features.merge(time_fea, on='CUST_NO', how='left')\n",
    "    all_features = all_features.merge(type_fea, on='CUST_NO', how='left')\n",
    "    all_features = all_features.merge(mcht_fea, on='CUST_NO', how='left')\n",
    "    all_features = all_features.merge(behavior_fea, on='CUST_NO', how='left')\n",
    "    all_features = all_features.merge(rfm_fea, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 填充缺失值\n",
    "    all_features = all_features.fillna(0)\n",
    "    \n",
    "    print(f\"最终特征数: {len(all_features.columns) - 1}\")\n",
    "    print(f\"客户数: {len(all_features)}\")\n",
    "    print(f\"数据形状: {all_features.shape}\")\n",
    "    \n",
    "    # 检查数据类型\n",
    "    print(f\"\\n特征数据类型分布:\")\n",
    "    print(all_features.dtypes.value_counts())\n",
    "    \n",
    "    return all_features\n",
    "\n",
    "# 合并训练集特征\n",
    "TRAIN_CCD_features = merge_all_features(\n",
    "    TRAIN_basic_fea, \n",
    "    TRAIN_time_fea, \n",
    "    TRAIN_type_fea, \n",
    "    TRAIN_mcht_fea, \n",
    "    TRAIN_behavior_fea, \n",
    "    TRAIN_rfm_fea\n",
    ")\n",
    "\n",
    "# 合并测试集特征\n",
    "A_CCD_features = merge_all_features(\n",
    "    A_basic_fea, \n",
    "    A_time_fea, \n",
    "    A_type_fea, \n",
    "    A_mcht_fea, \n",
    "    A_behavior_fea, \n",
    "    A_rfm_fea\n",
    ")\n",
    "\n",
    "print(\"\\n训练集特征预览:\")\n",
    "print(TRAIN_CCD_features.head())\n",
    "print(\"\\n测试集特征预览:\")\n",
    "print(A_CCD_features.head())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cc227cb3",
   "metadata": {},
   "source": [
    "### 保存训练集特征"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "5156bd3b",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "创建特征目录: ./feature/Train\n",
      "\n",
      "训练集特征文件已保存: ./feature/Train\\TRAIN_CCD_TR_DTL_features.pkl\n",
      "文件大小: 8.31 MB\n",
      "特征维度: (7332, 145)\n"
     ]
    }
   ],
   "source": [
    "# 确保特征目录存在\n",
    "feature_dir_train = './feature/Train'\n",
    "if not os.path.exists(feature_dir_train):\n",
    "    os.makedirs(feature_dir_train)\n",
    "    print(f\"创建特征目录: {feature_dir_train}\")\n",
    "\n",
    "# 保存为pickle格式\n",
    "output_file_train = os.path.join(feature_dir_train, 'TRAIN_CCD_TR_DTL_features.pkl')\n",
    "with open(output_file_train, 'wb') as f:\n",
    "    pickle.dump(TRAIN_CCD_features, f)\n",
    "\n",
    "print(f\"\\n训练集特征文件已保存: {output_file_train}\")\n",
    "print(f\"文件大小: {os.path.getsize(output_file_train) / 1024 / 1024:.2f} MB\")\n",
    "print(f\"特征维度: {TRAIN_CCD_features.shape}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "88ad7392",
   "metadata": {},
   "source": [
    "### 保存测试集特征"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "39df03f2",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 确保特征目录存在\n",
    "feature_dir_a = './feature/A'\n",
    "if not os.path.exists(feature_dir_a):\n",
    "    os.makedirs(feature_dir_a)\n",
    "    print(f\"创建特征目录: {feature_dir_a}\")\n",
    "\n",
    "# 保存为pickle格式\n",
    "output_file_a = os.path.join(feature_dir_a, 'A_CCD_TR_DTL_features.pkl')\n",
    "with open(output_file_a, 'wb') as f:\n",
    "    pickle.dump(A_CCD_features, f)\n",
    "\n",
    "print(f\"\\n测试集特征文件已保存: {output_file_a}\")\n",
    "print(f\"文件大小: {os.path.getsize(output_file_a) / 1024 / 1024:.2f} MB\")\n",
    "print(f\"特征维度: {A_CCD_features.shape}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fc32ce52",
   "metadata": {},
   "source": [
    "## 9. 特征统计信息"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "07f8cbe4",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"=\"*80)\n",
    "print(\"贷记卡交易明细表特征工程完成!\")\n",
    "print(\"=\"*80)\n",
    "\n",
    "print(\"\\n特征工程总结:\")\n",
    "print(f\"1. 基础统计特征: 交易笔数、金额统计量(均值、方差、偏度、峰度等)\")\n",
    "print(f\"2. 时间维度特征: 月度、周度、工作日/周末、时段分布\")\n",
    "print(f\"3. 交易类型特征: 交易码、交易方向、交易渠道分布\")\n",
    "print(f\"4. 商户维度特征: 商户数、商户集中度、商户消费分布\")\n",
    "print(f\"5. 消费行为特征: 大额/小额交易、Top交易集中度、交易间隔规律\")\n",
    "print(f\"6. RFM特征: 最近交易、交易频次、交易金额\")\n",
    "print(f\"7. 趋势特征: 近期vs早期消费对比、活跃度变化\")\n",
    "\n",
    "print(f\"\\n训练集:\")\n",
    "print(f\"  - 客户数: {len(TRAIN_CCD_features)}\")\n",
    "print(f\"  - 特征数: {len(TRAIN_CCD_features.columns) - 1}\")\n",
    "print(f\"  - 缺失值: {TRAIN_CCD_features.isnull().sum().sum()}\")\n",
    "\n",
    "print(f\"\\n测试集:\")\n",
    "print(f\"  - 客户数: {len(A_CCD_features)}\")\n",
    "print(f\"  - 特征数: {len(A_CCD_features.columns) - 1}\")\n",
    "print(f\"  - 缺失值: {A_CCD_features.isnull().sum().sum()}\")\n",
    "\n",
    "print(f\"\\n特征一致性检查:\")\n",
    "train_cols = set(TRAIN_CCD_features.columns)\n",
    "test_cols = set(A_CCD_features.columns)\n",
    "if train_cols == test_cols:\n",
    "    print(\"  训练集和测试集特征完全一致\")\n",
    "else:\n",
    "    print(f\"  警告: 训练集独有特征: {train_cols - test_cols}\")\n",
    "    print(f\"  警告: 测试集独有特征: {test_cols - train_cols}\")\n",
    "\n",
    "print(\"\\n特征列表(前20个):\")\n",
    "for i, col in enumerate(TRAIN_CCD_features.columns[1:21], 1):\n",
    "    print(f\"  {i}. {col}\")\n",
    "    \n",
    "print(\"  ...\")\n",
    "print(f\"  总计 {len(TRAIN_CCD_features.columns) - 1} 个特征\")\n",
    "\n",
    "print(\"\\n特征已成功保存到:\")\n",
    "print(f\"  - {output_file_train}\")\n",
    "print(f\"  - {output_file_a}\")\n",
    "\n",
    "print(\"\\n特征工程全部完成!\")\n",
    "print(\"=\"*80)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "starcup",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.18"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
